﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Red.Controls.Shell;
using Red.Core;
using Red.Core.Extensions;
using Red.Launcher.Utilities;
using Red.Launcher.ViewModels.Workspaces.Database;
using static System.Windows.Application;
using P4 = Red.Core.Perforce;
using Sql = Red.Launcher.ViewModels.Workspaces.Database.Sql;

namespace Red.Launcher.ViewModels.Workspaces
{
    public class Paths
    {
        public string GameRoot { get; set; }
        public string GameDepot { get; set; }
        public string EngineRoot { get; set; }
        public string EngineDepot { get; set; }
    }

    public class EnvironmentVariables
    {
        public string Root { get; set; }
        public Paths Paths { get; set; }
    }

    public class AppConfig
    {
        public Paths ParameterKey { get; set; }
        public EnvironmentVariables EnvironmentVariables { get; set; }
    }

    public class Manager : ViewModel, IEnvironmentalVariableSource
    {
        #region Private Data

        private bool _userFetchInProgress;
        private int _workspaceFetchesInProgress;

        private User _user;
        private Workspace _activeWorkspace;
        private AppConfig _appConfig;

        #endregion Private Data

        #region Public Properties

        public EnvironmentalVariableHelper EnvironmentalVariables { get; private set; } = new EnvironmentalVariableHelper();

        public AppConfig AppConfig
        {
            get { return _appConfig; }
            set
            {
                if ( _appConfig != value )
                {
                    _appConfig = value;

                    EnvironmentalVariables.Refresh().DoNotAwait();
                }
            }
        }

        public ObservableCollection<Workspace> Workspaces { get; private set; } = new ObservableCollection<Workspace>();

        public Workspace ActiveWorkspace
        {
            get { return _activeWorkspace; }
            set
            {
                if ( _activeWorkspace != value )
                {
                    _activeWorkspace = value;

                    if ( _activeWorkspace != null )
                    {
                        ActiveWorkspaceUId = _activeWorkspace.Uid;
                    }
                    else
                    {
                        ActiveWorkspaceUId = null;
                    }

                    OnPropertyChanged();
                    OnPropertyChanged( "ActiveWorkspaceName" );
                    EnvironmentalVariables.Refresh().DoNotAwait();
                }
            }
        }

        public string ActiveWorkspaceName
        {
            get
            {
                if ( ActiveWorkspace == null )
                {
                    return "No active Workspace";
                }
                else
                {
                    return ActiveWorkspace.DisplayName;
                }
            }
        }

        public string ActiveWorkspaceUId { get; set; }

        public User User
        {
            get { return _user; }
            set
            {
                if ( _user != value )
                {
                    _user = value;
                    OnPropertyChanged();
                }
            }
        }

        public P4Credential DefaultCredentials { get; set; }
        public P4.Mediator Mediator { get; private set; }

        public bool IsFetchInProgress => WorkspaceFetchesInProgress > 0 || IsUserFetchInProgress;

        public int WorkspaceFetchesInProgress
        {
            get { return _workspaceFetchesInProgress; }
            private set
            {
                if ( _workspaceFetchesInProgress != value )
                {
                    _workspaceFetchesInProgress = value;
                    OnPropertyChanged();
                    OnPropertyChanged( "IsFetchInProgress" );
                }
            }
        }

        public bool IsUserFetchInProgress
        {
            get { return _userFetchInProgress; }
            private set
            {
                if ( _userFetchInProgress != value )
                {
                    _userFetchInProgress = value;
                    OnPropertyChanged();
                    OnPropertyChanged( "IsFetchInProgress" );
                }
            }
        }

        #endregion Public Properties

        #region C-Tor

        public Manager()
        {
            EnvironmentalVariables.Source = this;
            Workspaces.CollectionChanged += Workspaces_CollectionChanged;
        }

        ~Manager()
        {
            EnvironmentalVariables.Shutdown();
        }

        #endregion C-Tor

        #region Public Methods

        #region User

        private void FetchUserCallback( Dictionary<string, object> row )
        {
            User = User.Create( row, true );
        }

        private void FetchCredentialsCallback( Dictionary<string, object> row )
        {
            DefaultCredentials = (P4Credential)P4Credential.METADATA.CreateFromDatabaseRow( row );
            Mediator = new P4.Mediator( Settings.PerforceServer, DefaultCredentials.Name, null );
        }

        public async Task<bool> FetchUser( string username )
        {
            if ( IsUserFetchInProgress )
                return true;

            IsUserFetchInProgress = true;

            DatabaseHelper.Result result;

            // Fetch User from database
            {
                Sql.Select.Table table = Sql.Select.CreateTable( User.METADATA.Table );
                Sql.Select query = new Sql.Select();
                query.Sources.Add( table );
                query.AddColumn( "*" );
                query.Conditions.AddLiteral( "username_win", username, table );

                result = await DatabaseHelper.FetchAsyncSafe( query.Sql, Settings.DatabaseConnectionString, FetchUserCallback, expectOne: true );

                if ( result != DatabaseHelper.Result.Success )
                {
                    RedMessageBox.Show( Current.MainWindow, RedDialogType.Ok, "Error fetching user from database", mode: RedMessageBox.Mode.Warning );
                    return false;
                }
            }

            // If there is no existing user in the database, add it
            if ( User == null )
            {
                Debug.Assert( User == null );

                Debug.Print( "Attempting to create database entry for user: " + username );

                string sql = $@"INSERT INTO [{Settings.DatabaseName}].[dbo].[Users]
                               ([username_win])
                           OUTPUT INSERTED.[id], INSERTED.[admin], INSERTED.[username_win]
                           VALUES
                               ('{username}')";

                result = await DatabaseHelper.FetchAsyncSafe( sql, Settings.DatabaseConnectionString, FetchUserCallback, expectOne: true );

                if ( result != DatabaseHelper.Result.Success )
                {
                    RedMessageBox.Show( Current.MainWindow, RedDialogType.Ok, "Error adding new user to database", mode: RedMessageBox.Mode.Warning );
                    return false;
                }
            }

            // Fetch default credentials
            {
                Sql.Select.Table table = Sql.Select.CreateTable( P4Credential.METADATA.Table, "a" );
                Sql.Select.Table defaultTable = Sql.Select.CreateTable( UserDefaultCredential.METADATA.Table, "b" );

                Sql.Select query = new Sql.Select();
                Sql.Select.Join defaultJoin = Sql.Select.CreateJoin( defaultTable, Sql.Select.JoinType.Inner );
                defaultJoin.Conditions.AddVar( "id", P4Credential.METADATA.ForeignKey, table, defaultTable );

                query.Sources.Add( table );
                query.Sources.Add( defaultJoin );

                query.AddColumn( "*", table );
                query.AddColumn( "id", defaultTable, "udc_id" );
                query.AddColumn( "[default]", defaultTable );
                query.Conditions.AddLiteral( "users_id", User.Id, table );

                result = await DatabaseHelper.FetchAsyncSafe( query.Sql, Settings.DatabaseConnectionString, FetchCredentialsCallback, expectOne: true );

                if ( result != DatabaseHelper.Result.Success )
                {
                    RedMessageBox.Show( Current.MainWindow, RedDialogType.Ok, "Error fetching default credentials", mode: RedMessageBox.Mode.Warning );
                    return false;
                }
            }

            if ( DefaultCredentials == null )
            {
                await User.Fetch( async () => await CheckForStandardCredentials( true ) );
            }

            // Notify system that user object has been retrieved and initialised
            OnFetchUserComplete?.Invoke();

            IsUserFetchInProgress = false;

            return true;
        }

        #endregion User

        #region Workspaces

        public async Task FetchWorkspaces()
        {
            if ( WorkspaceFetchesInProgress > 0 )
                return;

            if ( User == null )
                return;

            // We need to reset the active workspace so that we don't keep a handle around to an old stale object
            // But save and restore the active workspace id so that we can re-select it once it's re-fetched
            string activeWorkspaceUId = ActiveWorkspaceUId;
            ActiveWorkspace = null;
            ActiveWorkspaceUId = activeWorkspaceUId;

            Workspaces.Clear();

            WorkspaceFetchesInProgress = 3;

            Task[] fetchTasks = new Task[ WorkspaceFetchesInProgress ];

            fetchTasks[ 0 ] = FetchLocalWorkspaces();
            fetchTasks[ 1 ] = FetchAvailableWorkspaceSpecifications();
            fetchTasks[ 2 ] = FetchImplementedWorkspaceSpecifications();

            await Task.WhenAll( fetchTasks );
        }

        public async Task<Workspace> CreateWorkspaceInPerforce( SpecificationWorkspace specification, string destination )
        {
            bool success = await specification.CreateInPerforce( destination );

            if ( success )
            {
                ImplementedWorkspace workspace = new ImplementedWorkspace()
                {
                    WorkspaceInfo = specification.WorkspaceInfo,
                    Credentials = specification.Credentials,
                    ClientName = specification.PerforceName,
                    User = specification.User,
                    Revision = specification.Revision
                };

                int index = Workspaces.IndexOf( specification );

                Workspaces.RemoveAt( index );
                Workspaces.Insert( index, workspace );

                return workspace;
            }

            return null;
        }

        #endregion Workspaces

        #endregion Public Methods

        #region Private Methods

        #region User

        private P4Credential CreateCredentialsFromRow( Dictionary<string, object> row )
        {
            object credentialsId;
            if ( row.TryGetValue( "creds_id", out credentialsId ) )
            {
                if ( !credentialsId.Equals( DBNull.Value ) )
                {
                    return new P4Credential()
                    {
                        Id = (int)credentialsId,
                        Name = row[ "username" ] as string,
                        Password = row[ "password" ] as string
                    };
                }
            }

            return null;
        }
        
        private async Task CheckForStandardCredentials( bool makeDefault = false )
        {
            string standardCredential = User.UserPrincipal.SamAccountName;

            var defaults = from P4Credential credential in User.Children where credential.IsDefault select credential;
            var standards = from credential in User.Children where credential.Name == standardCredential select credential;

            bool hasDefault = defaults.Count() == 1;

            if( standards.Count() == 1 )
            {
                if( !hasDefault && makeDefault )
                {
                    P4Credential credential = (P4Credential)standards.First();
                    credential.IsDefault = true;
                    await User.Commit();
                }

                return;
            }

            //If we've reached here, that means this user does not have a standard credential
            P4Credential standard = new P4Credential()
            {
                Name = standardCredential
            };

            Record child = standard;
            User.AddChild( ref child );

            if ( !hasDefault && makeDefault )
            {
                standard.IsDefault = true;
            }

            await User.Commit();
        }

        #endregion User

        #region Workspaces

        private Task FetchLocalWorkspaces()
        {
            Sql.Select.Table clientSource = Sql.Select.CreateTable( P4ClientWorkspace.METADATA.Table, "client" );
            Sql.Select.Table credsSource = Sql.Select.CreateTable( P4Credential.METADATA.Table, "creds" );
            Sql.Select.Join credentialsJoin = Sql.Select.CreateJoin( credsSource );

            credentialsJoin.Conditions.AddVar( P4Credential.METADATA.ForeignKey, "id", clientSource, credsSource );

            Sql.Select query = new Sql.Select();
            query.Sources.Add( clientSource );
            query.Sources.Add( credentialsJoin );
            query.AddColumn( "*", clientSource );
            query.AddColumn( "id", credsSource, "creds_id" );
            query.AddColumn( "username", credsSource );
            query.AddColumn( "password", credsSource );
            query.Conditions.AddLiteral( User.METADATA.ForeignKey, User.Id );

            return DatabaseHelper.FetchAsync( query.Sql, Settings.DatabaseConnectionString, FetchLocalWorkspaceObject, FetchWorkspacesCompleteCallback );
        }

        private async Task FetchAvailableWorkspaceSpecifications()
        {
            string groupGuids = await CollateActiveDirectoryGroupGuids();

            string sql = $@"DECLARE @specs TABLE (id int primary key, revision int)
                            DECLARE @implemented TABLE (id int primary key)

                            INSERT INTO @specs
                                SELECT id, MAX(revision) revision
                                FROM [{Settings.DatabaseName}].[dbo].[P4ClientSpecs2]
                                GROUP BY id

                            INSERT INTO @implemented
                            SELECT p4specs_id
                                FROM [{Settings.DatabaseName}].[dbo].[ImplementedClientSpecs] as ics
                                WHERE ics.users_id = {User.Id} AND ics.host = '{Environment.MachineName}'

                            SELECT cs.*, dis.[disabled] FROM [{Settings.DatabaseName}].[dbo].[P4ClientSpecs2] as cs
                            INNER JOIN @specs AS s ON s.id = cs.id and s.revision = cs.revision
                            INNER JOIN [{Settings.DatabaseName}].[dbo].[UsersToClientSpecs] as ucs ON cs.id = ucs.p4specs_id
                            LEFT JOIN [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis ON cs.id = dis.p4specs_id AND dis.[disabled] != 'True'
                                AND dis.created_at = (SELECT MAX(created_at) AS created_at FROM [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis2 WHERE cs.id = dis2.p4specs_id)
                            WHERE ucs.users_id = {User.Id} AND cs.id NOT IN ( SELECT * FROM @implemented )

                            UNION

                            SELECT cs.*, dis.[disabled] FROM [{Settings.DatabaseName}].[dbo].[P4ClientSpecs2] as cs
                            INNER JOIN @specs AS s ON s.id = cs.id and s.revision = cs.revision
                            INNER JOIN [{Settings.DatabaseName}].[dbo].[GroupToClientSpecs] as gcs ON cs.id = gcs.p4specs_id
                            INNER JOIN [{Settings.DatabaseName}].[dbo].AssignedGroups as ag ON ag.group_id = gcs.group_id AND ag.users_id = {User.Id}
                            LEFT JOIN [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis ON cs.id = dis.p4specs_id
                                AND dis.created_at = (SELECT MAX(created_at) AS created_at FROM [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis2 WHERE cs.id = dis2.p4specs_id)
                            WHERE cs.id NOT IN ( SELECT * FROM @implemented ) AND ( dis.[disabled] = 'False' OR dis.[disabled] IS NULL )

                            UNION

                            SELECT cs.*, dis.[disabled] FROM [{Settings.DatabaseName}].[dbo].[P4ClientSpecs2] as cs
                            INNER JOIN @specs AS s ON s.id = cs.id and s.revision = cs.revision
                            INNER JOIN [{Settings.DatabaseName}].[dbo].[ADGroupToClientSpecs] as agcs ON cs.id = agcs.p4specs_id AND agcs.guid IN ({groupGuids})
                            LEFT JOIN [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis ON cs.id = dis.p4specs_id
                                AND dis.created_at = (SELECT MAX(created_at) AS created_at FROM [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis2 WHERE cs.id = dis2.p4specs_id)
                            WHERE cs.id NOT IN ( SELECT * FROM @implemented ) AND ( dis.[disabled] = 'False' OR dis.[disabled] IS NULL )";

            await DatabaseHelper.FetchAsync( sql, Settings.DatabaseConnectionString, FetchAvailableWorkspaceSpecificationObject, FetchWorkspacesCompleteCallback );
        }

        private Task<string> CollateActiveDirectoryGroupGuids()
        {
            return Task.Run( () =>
             {
                 var groups = ( (App)Current ).User.GetGroups();
                 var guids = from grp in groups select $"'{grp.Guid}'";

                 return string.Join( ", ", guids );
             } );
        }

        private Task FetchImplementedWorkspaceSpecifications()
        {
            string sql = $@"DECLARE @specs TABLE (id int primary key, revision int)

                            INSERT INTO @specs
                                SELECT id, MAX( revision ) revision
                                  FROM [{Settings.DatabaseName}].[dbo].[P4ClientSpecs2]
                                    GROUP BY id;

                            (SELECT cs.*, ics.host, ics.client_name AS implemented_client_name, creds.username, creds.password, s.revision as latest_revision, dis.[disabled]
                            FROM [{Settings.DatabaseName}].[dbo].[ImplementedClientSpecs] AS ics
                            LEFT JOIN [{Settings.DatabaseName}].[dbo].[P4ClientSpecs2] AS cs ON cs.id = ics.p4specs_id AND cs.revision = ics.revision
                            INNER JOIN [{Settings.DatabaseName}].[dbo].[P4Credentials] AS creds ON creds.id = ics.p4credentials_id
                            INNER JOIN @specs as s ON s.id = cs.id
                            LEFT JOIN [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis ON cs.id = dis.p4specs_id
                                AND dis.created_at = (SELECT MAX(created_at) AS created_at FROM [{Settings.DatabaseName}].[dbo].[DisabledClientSpecs] as dis2 WHERE cs.id = dis2.p4specs_id)
                            WHERE ics.users_id = '{User.Id}' AND ics.host = '{Environment.MachineName}' AND ( dis.[disabled] = 'False' OR dis.[disabled] IS NULL ) )";

            return DatabaseHelper.FetchAsync( sql, Settings.DatabaseConnectionString, FetchImplementedWorkspaceSpecificationObject, FetchWorkspacesCompleteCallback );
        }

        private async void Workspaces_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            if ( e.Action == NotifyCollectionChangedAction.Add )
            {
                foreach ( Workspace workspace in e.NewItems )
                {
                    await workspace.FetchParameters();

                    workspace.OnActive += workspace_OnActive;

                    if ( workspace.Uid == ActiveWorkspaceUId )
                    {
                        workspace.IsActive = true;
                    }
                }
            }
        }

        private void workspace_OnActive( Workspace workspace, bool isActive )
        {
            if ( isActive && ActiveWorkspace != null && ActiveWorkspace != workspace )
            {
                ActiveWorkspace.IsActive = false;
            }

            if ( workspace == ActiveWorkspace && !isActive )
            {
                ActiveWorkspace = null;
            }
            else
            {
                ActiveWorkspace = workspace;
            }

            if ( WorkspaceActive != null && isActive )
            {
                WorkspaceActive();
            }
        }

        private void FetchLocalWorkspaceObject( Dictionary<string, object> data )
        {
            LocalWorkspace workspace = new LocalWorkspace()
            {
                WorkspaceInfo = P4ClientWorkspace.METADATA.CreateFromDatabaseRow( data ),
                Credentials = CreateCredentialsFromRow( data )
            };

            Workspaces.Add( workspace );
        }

        private void FetchAvailableWorkspaceSpecificationObject( Dictionary<string, object> data )
        {
            SpecificationWorkspace workspace = new SpecificationWorkspace()
            {
                WorkspaceInfo = P4ClientSpecification.METADATA.CreateFromDatabaseRow( data ),
                Credentials = DefaultCredentials,
                User = User,
                Revision = (int)data[ "revision" ]
            };

            Workspaces.Add( workspace );
        }

        private void FetchImplementedWorkspaceSpecificationObject( Dictionary<string, object> data )
        {
            ImplementedWorkspace workspace = new ImplementedWorkspace()
            {
                WorkspaceInfo = P4ClientSpecification.METADATA.CreateFromDatabaseRow( data ),
                Credentials = DefaultCredentials,
                ClientName = (string)data[ "implemented_client_name" ],
                User = User,
                Revision = (int)data[ "revision" ],
                LatestRevision = (int)data[ "latest_revision" ]
            };

            Workspaces.Add( workspace );
        }

        private void FetchWorkspacesCompleteCallback( int resultsReturned, DatabaseHelper.Result error )
        {
            --WorkspaceFetchesInProgress;

            if ( error == DatabaseHelper.Result.Success && WorkspaceFetchesInProgress <= 0 )
            {
                OnFetchWorkspacesComplete?.Invoke();
            }
        }

        #endregion Workspaces

        #endregion Private Methods

        #region Events

        public delegate void SimpleEventDelegate();

        public event SimpleEventDelegate OnFetchUserComplete;
        public event SimpleEventDelegate OnFetchWorkspacesComplete;
        public event SimpleEventDelegate WorkspaceActive;

        #endregion Events

        #region IEnvironmentalVariableSource

        bool IEnvironmentalVariableSource.HasConfig => AppConfig?.EnvironmentVariables != null;
        bool IEnvironmentalVariableSource.HasValues => ActiveWorkspace != null;

        void IEnvironmentalVariableSource.SetEnvironmentVariables()
        {
            P4.Mediator mediator = new P4.Mediator( Settings.PerforceServer, ActiveWorkspace.Credentials.Name, ActiveWorkspace.PerforceName );
            string root = mediator.GetDepotRoot();

            Environment.SetEnvironmentVariable( AppConfig.EnvironmentVariables.Root, root, EnvironmentVariableTarget.User );
            SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.GameRoot, AppConfig.ParameterKey.GameRoot, ActiveWorkspace );
            SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.GameDepot, AppConfig.ParameterKey.GameDepot, ActiveWorkspace );
            SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.EngineRoot, AppConfig.ParameterKey.EngineRoot, ActiveWorkspace );
            SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.EngineDepot, AppConfig.ParameterKey.EngineDepot, ActiveWorkspace );

            // Delete the old obsolete var here
            Environment.SetEnvironmentVariable( "RL_WS_ROOT", null, EnvironmentVariableTarget.User );
        }

        void IEnvironmentalVariableSource.ClearEnvironmentVariables()
        {
            Environment.SetEnvironmentVariable( AppConfig.EnvironmentVariables.Root, null, EnvironmentVariableTarget.User );
            Environment.SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.GameRoot, null, EnvironmentVariableTarget.User );
            Environment.SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.GameDepot, null, EnvironmentVariableTarget.User );
            Environment.SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.EngineRoot, null, EnvironmentVariableTarget.User );
            Environment.SetEnvironmentVariable( AppConfig.EnvironmentVariables.Paths.EngineDepot, null, EnvironmentVariableTarget.User );
        }

        private void SetEnvironmentVariable( string var, string key, Workspace workspace )
        {
            string value;

            if ( ActiveWorkspace.Parameters.TryGetValue( key, out value ) )
            {
                Environment.SetEnvironmentVariable( var, value, EnvironmentVariableTarget.User );
            }
            else
            {
                Environment.SetEnvironmentVariable( var, null, EnvironmentVariableTarget.User );
            }
        }

        #endregion IEnvironmentalVariableSource
    }
}
